/*
 * Copyright (c) 2010-2011, Yawning <yawninglol at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package yawning.mew.sim;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.codehaus.commons.compiler.CompilerFactoryFactory;
import org.codehaus.commons.compiler.IClassBodyEvaluator;

import yawning.mew.MewPRNG;
import yawning.mew.MewView;
import yawning.mew.ProcEffect;
import yawning.mew.ProcEffect.Effect;
import yawning.mew.ProcEffect.Trigger;
import yawning.mew.PropertiesBase;
import yawning.mew.Report;
import yawning.mew.character.Toon;
import yawning.mew.sim.SimulationManager.Property;
import yawning.mew.sim.SimulationProc.EffectCallback;
import yawning.mew.sim.Strategy.IAction;
import yawning.mew.sim.Strategy.IStatus;
import yawning.mew.target.Target;

public final class SimulationEngine {
	private static final int SCACHE_MAX_SZ = 10;	// Probably can get away with a size of 1, but RAM is cheap so screw it.
	@SuppressWarnings("serial")
	private static Map<String,Strategy> sCache = new LinkedHashMap<String,Strategy>() {
		protected boolean removeEldestEntry(Map.Entry<String,Strategy> eldest) {
			return size() > SCACHE_MAX_SZ;
		}
	};

	public interface AfterActionsCallback {
		public void onAfterActions();
	}

	private class ActionWrapper {
		ActionWrapper(SimulationAction action, boolean isOnGCD) {
			mAction = action;
			mIsOnGCD = isOnGCD;
			mWatchdog = -1;
		}

		SimulationAction mAction;
		boolean mIsOnGCD;
		double mWatchdog;	// Used to detect off-GCD actions being called in a tight loop.
		int mWatchdogCount;
	}

	private Simulation mSim;
	private SimulationLog mLog;
	IStatus mStatus;
	private Toon mToon;
	private SimulationTarget mTarget;
	private PropertiesBase mConfig;
	private MewView mView;
	private MewPRNG mPrng;
	Strategy mStrategy;
	private boolean mIsLogging;
	private double mEndTime;
	private double mSimTime;
	private double mGCDTime;
	private double mLastEventTime;
	private Map<IAction,ActionWrapper> mActions = new HashMap<IAction,ActionWrapper>();
	private List<SimulationProc> mSimProcs = new ArrayList<SimulationProc>();
	private Map<Trigger, List<SimulationProc>> mSimProcsTriggers;
	private Map<String, SimulationProcStatus> mSimProcsStatusMap = new HashMap<String, SimulationProcStatus>();
	private List<AfterActionsCallback> mAfterActionsCallbacks = new ArrayList<AfterActionsCallback>();
	double mLinkedProcCDTime;
	private IAction mLastTriggeredAction = null;

	// Global Events.
	private SimulationEvent mHighResHzEv;
	private SimulationEventUtilityTimer mUtilityTimer;

	boolean mUseHighResHz;
	int mEventHashCode = 0;
	SimulationEventQueue mEventQueue = new SimulationEventQueue();
	HashMap<Effect,EffectCallback> mEffectCallbacks = new HashMap<Effect,EffectCallback>();

	SimulationEngine(Simulation sim, Toon toon, Target target, PropertiesBase config, MewView view, int seed, boolean saveLog) {
		mSim = sim;
		mToon = toon;
		mIsLogging = saveLog;
		if (mIsLogging) mLog = new SimulationLog(this);

		mTarget = new SimulationTarget(target, this);
		mConfig = config;
		mView = view;
		mPrng = new MewPRNG(config.getString(Property.SIMULATION_PRNG), seed);
		mEndTime = config.getDouble(Property.SIMULATION_DURATION);


		double jitter = mEndTime * config.getDouble(Property.SIMULATION_DURATION_JITTER) / 100;
		if (jitter > 0) {
			double g = mPrng.nextGaussian();
			if (g > 0.0) mEndTime += g * config.getDouble(Property.SIMULATION_DURATION_JITTER);
			if (g < 0.0) mEndTime /= (1 - g * config.getDouble(Property.SIMULATION_DURATION_JITTER) / mEndTime);
		}

		mHighResHzEv = new SimulationEvent(this) {};
		mUseHighResHz = config.getBool(Property.SIMULATION_HIGHRES_HZ);

		mUtilityTimer = new SimulationEventUtilityTimer(this);

		String strat = config.getString(Property.SIMULATION_STRATEGY);
		if (strat.length() > 0) setStrategy(strat);
	}

	public final PropertiesBase getConfig() {
		return mConfig;
	}

	public final Toon getToon() {
		return mToon;
	}

	public final SimulationTarget getPrimaryTarget() {
		return mTarget;
	}

	public final SimulationTarget getActiveTarget() {
		return mSim.getActiveTarget(mStatus);
	}

	public final MewView getView() {
		return mView;
	}

	public final MewPRNG getRandom() {
		return mPrng;
	}

	public final double getEndTime() {
		return mEndTime;
	}

	public final boolean hasStrategy() {
		return mStrategy != null;
	}

	public final void setStrategy(Strategy strat) {
		mStrategy = strat;
	}

	public final void setStrategy(InputStream stream) {
		// Load the default strategy, if any.  Throw a runtime exception if there isn't one.
		StringBuilder sb = new StringBuilder();
		try {
			BufferedReader reader = new BufferedReader(new InputStreamReader(stream));

			try {
				String line = null;
				while ((line = reader.readLine()) != null)
					sb.append(line).append('\n');
			} finally {
				reader.close();
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

		setStrategy(sb.toString());
	}

	private final void setStrategy(String strat) {
		synchronized (sCache) {
			if (sCache.containsKey(strat)) {
				Strategy mTemplate = sCache.get(strat);
				Class<? extends Strategy> clazz = mTemplate.getClass();
				try {
					mStrategy = clazz.newInstance();
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}

			if (mStrategy == null) {
				StringReader sr = new StringReader(strat);
				try {
					IClassBodyEvaluator cbe = CompilerFactoryFactory.getDefaultCompilerFactory().newClassBodyEvaluator();
					cbe.setExtendedClass(Strategy.class);
					Class<?>[] classes = mSim.getExportClasses();
					String[] classStrings = new String[classes.length + 1];
					classStrings[0] = "yawning.mew.sim.SimulationProcStatus";
					for (int i = 0; i < classes.length; i++) classStrings[i + 1] = classes[i].getCanonicalName();
					cbe.setDefaultImports(classStrings);
					mStrategy = (Strategy) cbe.createInstance(sr);
					sCache.put(strat, mStrategy);
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}
	}

	public final boolean isLogging() {
		return mIsLogging;
	}

	public final SimulationLog getLog() {
		return mLog;
	}

	public final void addAction(IAction action, SimulationAction r) {
		addAction(action, r, true);
	}

	public final void addAction(IAction action, SimulationAction r, boolean isOnGCD) {
		ActionWrapper w = new ActionWrapper(r, isOnGCD);
		mActions.put(action, w);
	}

	public final double getSimTime() {
		return mSimTime;
	}

	public final Report run() {
		mStatus = mSim.initSimulation(this);

		if (mStrategy == null) throw new RuntimeException("No valid strategies defined for Simulator!");

		// Use the High Resolution Timer Event to ensure that we query the strategy at least once.
		mHighResHzEv.schedule(0.0);

		// Call the optional script initialization hook.
		mSimTime = mGCDTime = 0.0;  // do this before the onSimulationBegin code
		mStrategy.onSimulationBegin(mStatus, mConfig.getString(Property.SIMULATION_STRATEGY_ARG));

		for (SimulationEvent ev = mEventQueue.first(); (mSimTime = ev.mNextTickTime) <= mEndTime; ev = mEventQueue.first()) {
			if (mSim.shouldQueryStrategy(mStatus)) {
				IAction action = mStrategy.getAction(mStatus);
				if (action != null) {
					ActionWrapper w = mActions.get(action);
					boolean isOnGCD = w.mIsOnGCD;
					if (!isOnGCD || mSimTime >= mGCDTime) {
						if (isOnGCD && mGCDTime >= mLastEventTime) mSimTime = mGCDTime;
						if (mSim.canPerformAction(mStatus, action)) {
							double gcd = w.mAction.perform();
							if (gcd != SimulationAction.ACTION_FAIL) mLastTriggeredAction = action;
							if (gcd == 0.0) {
								if (w.mWatchdog == mSimTime) {
									if (w.mWatchdogCount < 10) {
										w.mWatchdogCount++;
									} else {
										// Ugh, the script is doing something retarded.  Complain and move along with life.
										mView.onWarn("Watchdoged action: " + action);
										continue;
									}
								} else {
									w.mWatchdog = mSimTime;
									w.mWatchdogCount = 0;
									continue;
								}
							} else if (gcd > 0.0) {
								mGCDTime = mSimTime + gcd;
								mHighResHzEv.schedule(gcd);
							}
						}
					}
				}
			}

			// Call the various action complete hooks if any.
			if (!mAfterActionsCallbacks.isEmpty()) {
				for (AfterActionsCallback callback : mAfterActionsCallbacks)
					callback.onAfterActions();
				mAfterActionsCallbacks.clear();
			}

			if (mUseHighResHz && !mHighResHzEv.isUp()) mHighResHzEv.schedule(0.001);
			ev = mEventQueue.pollFirst();
			mLastEventTime = mSimTime = ev.mNextTickTime;
			ev.onTick();
			if (mSimTime >= ev.mExpiryTime) {
				ev.mUp = false;
				ev.mUpTime += mSimTime - ev.getScheduleTime();
				ev.onExpire();
			} else {
				ev.mNextTickTime = mSimTime + ev.mTickInterval;
				mEventQueue.add(ev);
			}

			// Prevent things from going horribly wrong if there are no events in the queue.
			// XXX: This is kind of slow.
			if (mEventQueue.isEmpty()) { mHighResHzEv.schedule(0.001); }
		}
		mSimTime = mEndTime;

		// Build the report, and append the log if it was requested.
		Report r = mSim.getReport(mStatus);
		if (mIsLogging) {
			r.setLog(mLog.toString());
		}

		return r;
	}

	public final boolean isInGCD() {
		return !(mSimTime >= mGCDTime);
	}

	// Force trigger a GCD.  Useful for synthetic events. (See yawning.mew.cat.EventRebirth)
	public final void triggerGCD(double gcd) {
		if (mSimTime < mGCDTime) {
			// We are already in a GCD.  This should NEVER happen in production releases.
			mView.onError("triggerGCD(" + gcd + ") invoked when in GCD. mSimTime: " + mSimTime + " mGCDTime: " + mGCDTime);
			throw new RuntimeException();
		} else {
			mGCDTime = mSimTime + gcd;
			mHighResHzEv.schedule(gcd);
		}
	}

	public final List<SimulationProc> getProcs() {
		return mSimProcs;
	}

	public final void addProc(SimulationProc proc) {
		if (mSimProcsTriggers == null) {
			mSimProcsTriggers = new HashMap<Trigger, List<SimulationProc>>();
			for (ProcEffect.Trigger t : ProcEffect.Trigger.values()) mSimProcsTriggers.put(t, new ArrayList<SimulationProc>());
		}

		mSimProcs.add(proc);
		proc.mSimEngine = this;
		for (Trigger t : proc.mEffect.triggers) {
			mSimProcsTriggers.get(t).add(proc);
		}

		if (!proc.mEffect.triggers.contains(Trigger.POTION)) {
			// Don't expose Trigger.POTION procs via a status object since we have a separate API for that.
			mSimProcsStatusMap.put(proc.mEffect.name, proc.mStatus);
		}
	}

	public final void triggerProc(SimulationTarget target, Trigger trigger) {
		if (mSimProcsTriggers != null) for (SimulationProc proc : mSimProcsTriggers.get(trigger)) proc.onProc(target);
	}

	public final void registerProcEffectCallback(Effect e, EffectCallback r) {
		mEffectCallbacks.put(e, r);
	}

	public final Map<String, SimulationProcStatus> getProcStatusMap() {
		return mSimProcsStatusMap;
	}

	public final void addAfterActionCallback(AfterActionsCallback callback) {
		mAfterActionsCallbacks.add(callback);
	}

	public final SimulationEventUtilityTimer getUtilityTimer() {
		return mUtilityTimer;
	}

	public final IAction getLastTriggeredAction() {
		return mLastTriggeredAction;
	}
}
